<?php

require_once(dirname(__FILE__).'/module_base.php');
require_once(dirname(__FILE__).'/resourcepage_container.php');
require_once(dirname(__FILE__).'/resourcepage_canned.php');
define('DASHBOARD_RESOURCEPAGE_EDIT',7122);
/**
 * Main class for the resourcepage module which provides a list of files or
 * links that can easily be created and configured. Supports two types of
 * extension - 'push' and 'pull' extensions - that provide additional
 * content for the list.
 *
 * All access to the resourcepage tables (except for Moodle core's use of
 * the main prefix_resourcepage table) should be done via methods in this
 * class. SQL statements that access the resourcepage tables should not be
 * included in any other class.
 *
 * @copyright &copy; 2006 The Open University
 * @author s.marshall@open.ac.uk
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
 * @package resourcepage
 */
class module_resourcepage extends module_base {

    /**
     * Obtain filepath used to store files for a particular course.
     * @param object $course Course object
     * @return string Folder path (without final /)
     */
    public function get_save_folder($course) {
        // We save in the module data folder for this module. This folder
        // is protected from ordinary download by changes to files.php.
        global $CFG;
        $folder=$CFG->dataroot.'/'.$course->id.'/moddata/resourcepage';
        if(!mkdir_recursive($folder)) {
            error('Unable to create upload folder');
        }
        return $folder;
    }

    public function get_database_version() {

        return "2007022800";
    }

    private $partialupgradedate=null;

    /**
     * Overridable method that gets called after adding an
     * instance record. You can use this if some setup needs to be
     * done after the instance ID is known.
     *
     * The base version does nothing.
     * @param $id ID of module
     * @param $content Object containing form fields from the setup page.
     */
    protected function after_add_instance($id,&$content) {
        // Make the two containers
        $containerid=$this->add_container('_page',$id);
        $deletedcontainerid=$this->add_container('_deleted',$id);

        // Add container id and deleted container id to content for restore
        // Consider update instance to update resourcepage table (set $content->instance)
        $content->containerid = $containerid;
        $content->deletedcontainerid = $deletedcontainerid;

        // Store them in the table
        $rs=$this->do_sql("
UPDATE prefix_resourcepage
SET containerid=$containerid,deletedcontainerid=$deletedcontainerid
WHERE id=$id");
    }


    protected function before_deleting_instance($record) {
        // Clear out containers so I can delete them first
        $this->do_sql("UPDATE prefix_resourcepage SET containerid=NULL,deletedcontainerid=NULL WHERE id={$record->id}");
        $rs=$this->do_sql("SELECT id FROM modprefix_containers WHERE resourcepageid={$record->id}");
        while(!$rs->EOF) {
            $this->delete_container_with_items($rs->fields['id']);
            $rs->MoveNext();
        }

        // Tell extensions to delete any references
        foreach($this->get_extensions() as $extension) {
            $extension->before_deleting_resourcepage($record);
        }
        $this->do_sql('DELETE FROM modprefix_cannedusage WHERE resourcepageid='.$record->id);
    }

    /**
     * Deletes an item and any items based on it.
     * @param $itemid Item ID
     */
    public function delete_item($itemid) {
        // Query that finds all items based on this
        // or based on items in this container
        $subquery=<<<ENDQUERY
SELECT id
FROM modprefix_items
WHERE basedonitemid=$itemid OR id=$itemid
ENDQUERY;

        $tw=new transaction_wrapper();
        $this->delete_item_bitses($subquery);
        $tw->commit();
    }

    /**
     * Deletes item from container, but keeps the actual item around so it
     * can be added or inserted again.
     */
    public function delete_container_item($container,$item) {
        $this->delete_container_items("containerid=$container AND itemid=$item");
    }

    /**
     * Deletes subcontainer from container, but keeps the actual container around so it
     * can be added or inserted again.
     */
    public function delete_container_subcontainer($container,$subcontainer) {
        $this->delete_container_items("containerid=$container AND subcontainerid=$subcontainer");
    }

    /**
     * Deletes some entries from modprefix_containeritems, maintaining the
     * sequence numbering.
     */
    private function delete_container_items($condition) {
        $tw=new transaction_wrapper();

        // Entries in containeritems; retain sequence numbering
        $rs=$this->do_sql("SELECT containerid,sequence FROM modprefix_containeritems WHERE $condition ORDER BY containerid,sequence DESC");
        // Process results
        $actions=array();
        $lastcontainer=0; $lastsequence=0; $currentoffset=0;
        while(true) {

            $container=$rs->EOF ? 0 : $rs->fields['containerid'];
            $sequence=$rs->EOF ? 0 : $rs->fields['sequence'];

            if($container==$lastcontainer && $sequence==$lastsequence-1) {
                // Continuing a consecutive range
                $lastsequence=$sequence;
                $currentoffset++;
                $rs->MoveNext();
                continue;
            }

            if ($currentoffset>0) {
                // Apply the saved changes, we're going onto a different range or container

                // Note that the following arsing about is only necessary to avoid a clash if rows are
                // out of sequence order when it updates. For example if it comes to sequence 5 before
                // sequence 3 and tries to apply a -2, this would break as sequence 3 already exists.
                // In practice it's very hard to make the database do this (it always accesses them
                // on order of container/sequence, which is the primary key) but theoretically it
                // could happen and in practice it happened once.
                $actions[]='UPDATE modprefix_containeritems SET sequence=sequence+10000 '.
                    ' WHERE containerid='.$lastcontainer.' AND sequence > '.$lastsequence;
                $actions[]='UPDATE modprefix_containeritems SET sequence=sequence-'.(10000+$currentoffset).
                    ' WHERE containerid='.$lastcontainer.' AND sequence > 10000';
            }

            if($rs->EOF) {
                break;
            }

            $currentoffset=1;
            $lastcontainer=$container;
            $lastsequence=$sequence;

            $rs->MoveNext();
        }

        // Delete unwanted items
        $this->do_sql("DELETE FROM modprefix_containeritems WHERE $condition");

        // Apply queries that shuffle other items
        foreach($actions as $action) {
            $this->do_sql($action);
        }

        $tw->commit();
    }

    /**
     * Deletes all item dependencies, and items themselves, for items within the subquery.
     * @param string $subquery SQL query that returns one column of items
     */
    private function delete_item_bitses($subquery) {
        // Get list of items (note: need to do this separately not as a literal
        // subquery because it may start returning different values after we've
        // done some of the deletes)
        $rs=$this->do_sql($subquery);
        $items='';
        while(!$rs->EOF) {
            if($items!='') {
                $items.=',';
            }
            $items.=$rs->fields['id'];
            $rs->MoveNext();
        }

        if(strlen($items)==0) {
            return;
        }

        // Flags
        $this->do_sql("DELETE FROM modprefix_itemflags WHERE itemid IN ($items)");
        // Files
        $this->do_sql("DELETE FROM modprefix_files WHERE itemid IN ($items)");
        // Container items
        $this->delete_container_items("itemid IN ($items)");
        // Placeholder entries
        $rs=$this->do_sql("
SELECT id,placeholder
FROM modprefix_items
WHERE id IN ($items) AND placeholder IS NOT NULL");
        while(!$rs->EOF) {
            // If extension exists, inform it that placeholder was deleted
            try {
                // Need to not do this during halfway through upgrades (sigh)
                if(empty($this->partialupgradedate) || $this->partialupgradedate>'2006032802') {
                    $this->get_extension_for_placeholder($rs->fields['placeholder'])->
                        delete_instance($rs->fields['id']);
                }
            } catch(Exception $e) {
                // If the extension doesn't exist, don't stress
            }
            $rs->MoveNext();
        }
        // Placeholder items
        $this->do_sql("DELETE FROM modprefix_placeholderitms WHERE placeholderitemid IN ($items)");
        // The items themselves
        $this->do_sql("DELETE FROM modprefix_items WHERE id IN ($items)");
    }

    /**
     * Removes an item from one container then adds it to another (or possibly
     * to the same one again)
     * @param int $container ID of item's parent container
     * @param int $item ID of item
     * @param int $newcontainer ID of target container
     * @param int $newseq Sequence in new container (leave 0/default to add to end)
     */
    public function move_container_item($container,$item,$newcontainer,$newseq=0) {
        $tw=new transaction_wrapper();

        // Get current other fields (hidden)
        $rs=$this->do_sql(
            'SELECT hidden FROM modprefix_containeritems'.
            ' WHERE itemid='.sql_int($item).
            ' AND containerid='.sql_int($container));
        $hidden=(bool)$rs->fields['hidden'];

        // Delete entry
        $this->delete_container_items('itemid='.sql_int($item).
            ' AND containerid='.sql_int($container));
        if($newseq==0) {
            // Add it again (this gives it new sequence etc at end)
            $this->add_container_item($newcontainer,$item,$hidden);
        } else {
            // Insert in specified sequence
            $this->insert_container_item($newcontainer,$item,$newseq,$hidden);
        }

        $tw->commit();
    }

    /**
     * Removes an item from one container then adds it to another (or possibly
     * to the same one again)
     * @param int $container ID of subcontainer's parent container
     * @param int $subcontainer ID of container being moved
     * @param int $newcontainer ID of target container
     * @param int $newseq Sequence in new container (leave 0/default to add to end)
     */
    public function move_container_subcontainer($container,$subcontainer,$newcontainer,$newseq=0) {
        $tw=new transaction_wrapper();

        // Get current other fields (hidden)
        $rs=$this->do_sql(
            'SELECT hidden FROM modprefix_containeritems'.
            ' WHERE subcontainerid='.sql_int($subcontainer).
            ' AND containerid='.sql_int($container));
        $hidden=(bool)$rs->fields['hidden'];

        // Delete entry
        $this->delete_container_items('subcontainerid='.sql_int($subcontainer).
            ' AND containerid='.sql_int($container));
        if($newseq==0) {
        // Add it again (this gives it new sequence etc)
            $this->add_container_subcontainer($newcontainer,$subcontainer,$hidden);
        } else {
            $this->insert_container_subcontainer($newcontainer,$subcontainer,$newseq,$hidden);
        }

        $tw->commit();
    }

    /**
     * Sets the 'hidden' flag for an item in a container.
     * @param int $container ID of item's parent container
     * @param int $item ID of item
     * @param bool $hidden New value for hidden flag
     */
    public function set_container_item_hidden($container,$item,$hidden) {
        $this->do_sql(
            'UPDATE modprefix_containeritems'.
            ' SET hidden='.sql_int($hidden).
            ' WHERE itemid='.sql_int($item).
            ' AND containerid='.sql_int($container));
    }

    /**
     * Sets the 'hidden' flag for a subcontainer in a container.
     * @param int $container ID of subcontainers parent container
     * @param int $subcontainer ID of the subcontainer
     * @param bool $hidden New value for hidden flag
     */
    public function set_container_subcontainer_hidden($container,$subcontainer,$hidden) {
        $this->do_sql(
            'UPDATE modprefix_containeritems'.
            ' SET hidden='.sql_int($hidden).
            ' WHERE subcontainerid='.sql_int($subcontainer).
            ' AND containerid='.sql_int($container));
    }

    /**
     * Deletes all items from a container, but leaves the container
     * itself.
     */
    public function delete_contained_items($containerid) {
        assert (isset($containerid));

        // Query that finds all items for this container
        // or based on items in this container
        $subquery=<<<ENDQUERY
SELECT i.id
FROM modprefix_containeritems ci
LEFT JOIN modprefix_items i ON ci.itemid=i.basedonitemid
WHERE ci.containerid=$containerid AND i.id IS NOT NULL
UNION
SELECT i.id
FROM modprefix_containeritems ci
LEFT JOIN modprefix_items i ON ci.itemid=i.id
WHERE ci.containerid=$containerid AND i.id IS NOT NULL
ENDQUERY;


        $tw=new transaction_wrapper();
        // Delete item bits
        $this->delete_item_bitses($subquery);

        // Delete any subcontainers but only if they belong to the same
        // resourcepage (ie not shared pages)
        $rs=$this->do_sql("
SELECT
    cchild.id
FROM
    modprefix_containeritems ci
    INNER JOIN modprefix_containers cthis ON cthis.id=ci.containerid
    INNER JOIN modprefix_containers cchild ON cchild.id=ci.subcontainerid
WHERE
    ci.containerid=$containerid AND cchild.resourcepageid=cthis.resourcepageid
    ");
        while($rec=rs_fetch_next_record($rs)) {
            $this->delete_container_with_items($rec->id);
        }
        rs_close($rs);

        $tw->commit();
    }

    /**
     * Deletes a container and all its associated items (and any
     * other items that are based on those).
     * @param int $containerid Container ID
     */
    public function delete_container_with_items($containerid) {

        $rs=$this->do_sql("SELECT a.* FROM modprefix_containeritems a, modprefix_containers b WHERE a.subcontainerid=".$containerid." and b.id = a.subcontainerid and b.containertype='pull_rssfeed'");
        if ($rs->_numOfRows >= 1) {

            /*
             * If the sub-container is an RSS feed, then we dont delete the RSS container; just references to it.
             * This allows shared RSS feeds to continue to exists.
             *
             */
            $tw=new transaction_wrapper();
            $this->delete_container_subcontainer($rs->fields['containerid'],$containerid);
            $tw->commit();

            /*
             * If the RSS container is orphaned; i.e. no resourcepage has it as a child item, then
             * we will delete them proper...
             *
             */
            $rs=$this->do_sql("SELECT * FROM modprefix_x_rssfeed WHERE containerid NOT IN (SELECT DISTINCT subcontainerid FROM modprefix_containeritems WHERE subcontainerid IS NOT null)");
            $tw=new transaction_wrapper();
            while(!$rs->EOF) {
                $containerid=$rs->fields['containerid'];
                $this->delete_contained_items($containerid);
                // Delete references to container
                $this->delete_container_items("containerid=$containerid OR subcontainerid=$containerid");
                // Tell extensions to delete any references
                foreach($this->get_extensions() as $extension) {
                    $extension->before_deleting_container($containerid);
                }
                // And the container
                $this->do_sql("DELETE FROM modprefix_containers WHERE id=$containerid");

                $rs->MoveNext();
            }
            $tw->commit();

        }  else {

            $tw=new transaction_wrapper();
            $this->delete_contained_items($containerid);
            // Delete references to container
            $this->delete_container_items("containerid=$containerid OR subcontainerid=$containerid");
            // Tell extensions to delete any references
            foreach($this->get_extensions() as $extension) {
                $extension->before_deleting_container($containerid);
            }
            // And the container
            $this->do_sql("DELETE FROM modprefix_containers WHERE id=$containerid");
            $tw->commit();

        }

    }

    /**
     * Runs an SQL statement, automatically replacing modprefix_ with the module's prefix,
     * and prefix_ with the base Moodle prefix. Throws an exception if there is any SQL error.
     * @param string $sql SQL statement
     * @return object ADODB result set
     */
    public function do_sql($sql, $trace = false) {
        global $CFG, $db;
        $sql=preg_replace('/modprefix\_/',$CFG->prefix.'resourcepage_',$sql);
        $sql=preg_replace('/prefix\_/',$CFG->prefix,$sql);

        if($trace) {
            print '<pre>'.htmlspecialchars($sql).'</pre>';
        }

        if(!$rs=get_recordset_sql($sql,false)) {
            throw new Exception('module_resourcepage SQL error: '.
                $db->ErrorMsg());
        }
        return $rs;
    }

    public function add_flag($flag) {
        $this->do_sql(
            'INSERT INTO modprefix_flags(flag) '.
            'VALUES ('.$this->c($flag).')');
    }

    /**
     * Based on data found in mdl_resourcepage, lists all the *other* resourcepages
     * on a particular course.
     * @param int $courseid Course ID
     * @param int $moduleid Module ID to exclude from list; if omitted, will list all pages
     * @return array Associative array from ID (int) => name of page (string)
     */
    public function get_other_resourcepages($courseid,$moduleid=0) {
        $rs=$this->do_sql(
            'SELECT id,name FROM prefix_resourcepage WHERE course='.$courseid.
            ' AND id<>'.$moduleid.' ORDER BY name');
        return $rs->GetAssoc();
    }

    /**
     * Adds a new placeholder string (used only when installing push extensions).
     * @param string $placeholder Placeholder ID string
     */
    public function add_placeholder($placeholder,$extension) {
        $this->do_sql(
            'INSERT INTO modprefix_placeholders(placeholder,extension) '.
            'VALUES ('.$this->c($placeholder).','.$this->c($extension).')');
    }

    /**
     * Deletes an entire placeholder and all items created by it (used only
     * when uninstalling push extensions).
     * @param string $placeholder Placeholder ID string
     */
    public function delete_placeholder($placeholder) {
        $tw=new transaction_wrapper();
        $phsql=$this->c($placeholder);

        // Get rid of the actual placeholder items
        $subquery=<<<ENDQUERY
SELECT id
FROM modprefix_items
WHERE placeholder=$phsql
ENDQUERY;
        $this->delete_item_bitses($subquery);

        // Delete placeholderitms pointers
        $this->do_sql(
            'DELETE FROM modprefix_placeholderitms WHERE (SELECT placeholder FROM modprefix_items WHERE id=placeholderitemid)='.$phsql);

        // Delete placeholder entries
        $this->do_sql(
            'DELETE FROM modprefix_placeholders WHERE placeholder='.$phsql);
        $tw->commit();
    }

    /**
     * Returns list of flags.
     * @return array Array of string flag IDs.
     */
    public function get_available_flags() {
        // List of flags is obtained from the flag language files (kind of ugh especially since those
        // are all stored in the arse-end of nowhere)
        $result=array();
        foreach(glob(dirname(__FILE__).'/lang/en_utf8/resourcepage_flag_*.php') as $flaglangfile) {
            $result[]=preg_replace('/^.*resourcepage_flag_(.*)\.php$/','$1',$flaglangfile);
        }
        return $result;
    }

    /**
     * Checks if specified flag is applicable to specified item
     * @param string $itemtype Item type (e.g. 'text', 'weblink', 'file')
     * @param string $flag Flag name (e.g. 'inaccessible', 'printeditem')
     * @return bool True if specified flag is applicable to specified item
     */
    public function is_item_flag($itemtype, $flag) {
        return strpos(','.get_string('itemlist', 'resourcepage_flag_'.$flag, NULL, dirname(__FILE__).'/lang/').',', ','.$itemtype.',') === false ? false : true;
    }

    /**
     * Checks if user can edit specified resourcepage module flag
     * @param string $flag Flag name (e.g. 'inaccessible', 'printeditem')
     * @return bool True if user can edit specified resourcepage module flag
     */
    public function user_can_edit_item_flag($flag) {
        // Check for Admin editing restriction (capability in the future)
        $flagcap = get_string('flagcap', 'resourcepage_flag_'.$flag, NULL, dirname(__FILE__).'/lang/');
        if (strpos($flagcap, 'mo') === false) {
            // No Admin editing restriction
            return true;
        }
        // Otherwise return admin flag
        return isadmin();
    }

    /**
     * Obtains localised text string for the given flag.
     * @param string $flag Flag ID
     * @param bool $on True if you want the text for flag being on
     * @return string Requested text
     */
    public function get_flag_text($flag,$on) {
        return get_string(($on ? 'editingon' : 'editingoff'),'resourcepage_flag_'.$flag, NULL, dirname(__FILE__).'/lang/');
    }

    /**
     * Gets 'extra info' for the given flag. This will be placed in brackets after
     * the item title.
     * @param string $flag Flag ID
     * @return string Extra info or false if none
     */
    public function get_flag_extrainfo($flag) {
        $result=get_string('extrainfo','resourcepage_flag_'.$flag, NULL, dirname(__FILE__).'/lang/');
        if(preg_match('/^\[\[.*\]\]$/',$result)) {
            return false;
        }
        return $result;
    }

    /**
     * Gets icon information for the given flag. The actual icon, if present, will be
     * found in mod/resourcepage/pix/i or {$CFG->pixpath}/resourcepage_flags.
     * @param string $flag Flag ID
     * @return object Simple object with icon details ->exists (boolean) and ->alt (string,
     *   empty if none).
     */
    public function get_flag_icon_info($flag) {
        $result=new stdClass;

        global $CFG;

        // Look in theme first
        $result->src=$CFG->pixpath.'/resourcepage_flags/'.$flag.'.gif';
        $result->exists=file_exists(str_replace($CFG->wwwroot,$CFG->dirroot,$result->src));

        // Otherwise default to inside module
        if(!$result->exists) {
            $result->src=$CFG->wwwroot.'/mod/resourcepage/pix/resourcepage_flags/'.$flag.'.gif';
            $result->exists=file_exists(str_replace($CFG->wwwroot,$CFG->dirroot,$result->src));
        }

        if($result->exists) {
            $result->alt=get_string('iconalt','resourcepage_flag_'.$flag, NULL, dirname(__FILE__).'/lang/');
            if(preg_match('/^\[\[.*\]\]$/',$result->alt)) {
                $result->alt='';
            }
        } else {
            $result->alt='';
        }

        return $result;
    }

    /**
     * Obtains a list of available placeholder IDs and which extensions
     * provide them.
     * @return array Associative array of placeholder ID string => extension object
     */
    public function get_available_placeholders() {
        $extensions=$this->get_extensions();
        $placeholders=array();
        foreach($extensions as $extension) {
            if(is_a($extension,'resourcepage_extension_push')) {
                $thisplaceholders=$extension->get_placeholders();
                foreach($thisplaceholders as $placeholder) {
                    $placeholders[$placeholder]=$extension;
                }
            }
        }
        return $placeholders;
    }

    /**
     * Obtains the extension object that corresponds to a particular placeholder.
     * @param string $placeholder Placeholder ID string
     * @return resourcepage_extension_push Extension object
     * @throws Exception if it doesn't exist
     */
    public function get_extension_for_placeholder($placeholder) {
        $extensions=$this->get_extensions();
        foreach($extensions as $extension) {
            if(is_a($extension,'resourcepage_extension_push')) {
                if(in_array($placeholder,$extension->get_placeholders())) {
                    return $extension;
                }
            }
        }
        throw new Exception('Failed to find extension for placeholder '.$placeholder);
    }

    /**
     * Gets display information for a particular placeholder.
     * @param string $placeholder Placeholder ID
     * @param int $placeholderitemid ID of this individual placeholder
     * @return object Object containing fields 'title' and 'description'.
     */
    public function get_placeholder_display_details($placeholder,$placeholderitemid) {
        $extensions=$this->get_extensions();
        foreach($extensions as $extension) {
            if(is_a($extension,'resourcepage_extension_push')) {
                $thisplaceholders=$extension->get_placeholders();
                foreach($thisplaceholders as $thisplaceholder) {
                    if($thisplaceholder==$placeholder) {
                        $return=new stdClass;
                        $return->title=$extension->get_placeholder_title($placeholder,$placeholderitemid);
                        $return->description=$extension->get_placeholder_description($placeholder,$placeholderitemid);
                        $return->editable=$extension->has_edit_page();
                        $return->extension=$extension->get_name();
                        return $return;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Sets plugin info field on a given item.
     * @param int $itemid Iten integer ID
     * @param string $text Text to set, or null to clear field
     */
    public function set_plugin_info($itemid,$text) {
        $this->do_sql('UPDATE modprefix_items SET plugininfo='.$this->c($text).' WHERE id='.$itemid);
    }

    /**
     * Obtains all placeholders of the given type. This is a rather slow
     * query that ends up going through all items in the database, so don't
     * use too often.
     * @param string $placeholder Placeholder ID
     * @param int $courseid If set, looks only in specified course
     * @return array Array of objects, fields 'placeholderitemid' and 'courseid'.
     */
    public function get_placeholders($placeholder,$courseid=0) {
        if($courseid) {
            $extraquery="AND rp.course=$courseid";
        } else {
            $extraquery='';
        }
        $rs=$this->do_sql(<<<ENDQUERY
SELECT i.id,rp.course,i.plugininfo
FROM prefix_resourcepage rp
INNER JOIN modprefix_containeritems ci1 ON rp.containerid=ci1.containerid OR rp.deletedcontainerid=ci1.containerid
LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
INNER JOIN modprefix_items i ON COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=i.id
WHERE i.placeholder='$placeholder' $extraquery
ENDQUERY
            );
        $result=array();
        while(!$rs->EOF) {
            $entry=new stdClass;
            $entry->placeholderitemid=$rs->fields['id'];
            $entry->courseid=$rs->fields['course'];
            $entry->plugininfo=$rs->fields['plugininfo'];
            $result[]=$entry;

            $rs->MoveNext();
        }

        return $result;
    }

    /**
     * Obtains a list of items that have been added to a particular placeholder.
     * Even if the items have since been deleted, it still returns
     * them.
     * @param string $placeholderitemid Placeholder item's ID
     * @return array Array of base item IDs - these are the base items, not the aliases
     *   that were actually added to that course.
     */
    public function get_placeholder_added_items($placeholderitemid) {
        $query=<<<ENDQUERY
SELECT
  pi.baseitemid
FROM
  modprefix_placeholderitms pi
WHERE pi.placeholderitemid=$placeholderitemid
ENDQUERY;
        $rs=$this->do_sql($query);

        $result=array();
        while(!$rs->EOF) {
            $result[]=$rs->fields['baseitemid'];
            $rs->MoveNext();
        }
        return $result;
    }

    /**
     * Obtains a list of items that have been added to a particular placeholder in a
     * particular course. Even if the items have since been deleted, it still returns
     * them.
     * @param string $placeholderitemid Placeholder item's ID
     * @param string $joinwith Full name of table to join with. Table must include an
     *   itemid field. Can include prefix_ and modprefix_ in name.
     * @param string $itemidfield Optionally include alternate name for 'itemid' field
     *   in the matched table.
     * @return array Array of objects, each one contains fields matching lines in the
     *   table that was joined with. Key of arrays is the ID field.
     */
    public function get_placeholder_added_items_plus($placeholderitemid,$joinwith,$itemidfield='itemid') {
        $query=<<<ENDQUERY
SELECT
  t.*
FROM
  modprefix_placeholderitms pi
  INNER JOIN $joinwith t ON pi.baseitemid=t.$itemidfield
WHERE
  pi.placeholderitemid=$placeholderitemid
ENDQUERY;
        $rs=$this->do_sql($query);

        $result=array();
        while(!$rs->EOF) {
            $objectversion=new stdClass;
            foreach($rs->fields as $key=>$value) {
                if(preg_match('/[^0-9]/',$key)) {
                    $objectversion->{strtolower($key)}=$value;
                }
            }
            $result[$rs->fields[$itemidfield]]=$objectversion;
            $rs->MoveNext();
        }
        return $result;
    }

    /**
     * Adds a placeholder instance to a particular container and initialises
     * its settings.
     * @param string $placeholder Placeholder ID
     * @param int $courseid Course ID
     * @param int $containerid Container ID that placeholder will be added to
     */
    public function add_placeholder_instance($placeholder,$courseid,$containerid) {
        // Add placeholder...
        $added=$this->add_item(
            'placeholder',true,null,null,null,null,null,null,null,$placeholder,null);
        // ...to container...
        $this->add_container_item($containerid,$added);
        // ...and let it create its settings
        $this->get_extension_for_placeholder($placeholder)->
            add_instance($added,$placeholder,$courseid);
    }

    /**
     * Adds an item to the given placeholder on the given course. This will
     * create a new item based on the given base item, add it to just above
     * the placeholder in _containeritems, and add it to _placeholderitms.
     * @param int $courseid Course ID
     * @param string $placeholder Placeholder ID
     * @param int $baseitemid ID of base item (must have already been created)
     * @param int $livedate Live date (in days since start of course)
     * @param int $removedate Remove date (in days since start of course)
     * @param int $showto Permission strings
     * @param bool $entryonly If true, only adds the entry linking the placeholder
     *   to the base item, and doesn't create an actual item
     * @return int ID of newly-created alias item
     */
    public function add_placeholder_item($placeholderitemid,$baseitemid,
        $livedate=null,$removedate=null,$showto=null,$entryonly=false) {
        $tw=null;
        if(!$entryonly) {
            $tw=new transaction_wrapper();
        }

        // Add to _placeholderitms
        $this->do_sql(
            'INSERT INTO modprefix_placeholderitms(placeholderitemid,baseitemid) VALUES('.
            $placeholderitemid.','.$baseitemid.')');

        if($entryonly) {
            return true;
        }

        // Create alias
        $newitem=$this->add_item(null,true,$livedate,$removedate,$showto,
            null,null,null,null,null,$baseitemid,null,null,null);

        // Find position of existing placeholder
        $rs=$this->do_sql(
            'SELECT containerid,sequence FROM modprefix_containeritems WHERE itemid='.
            $placeholderitemid);
        if($rs->EOF) {
            $tw->rollback();
            throw new Exception('Couldn\'t find expected placeholder');
        }

        // Add into container
        $this->insert_container_item($rs->fields['containerid'],$newitem,$rs->fields['sequence']);

        $tw->commit();
        return $newitem;
    }

    public function add_item(
        $itemtype,$editable,
        $livedate,$removedate,$showto,
        $title,$description,
        $plugininfo,
        $weblink,$placeholder,$basedonitem,$crosslink=null,$headinglevel=null, $options=null, $rights=null, $data=null) {

		// Use insert_record for database independence
/*
        $this->do_sql(
            'INSERT INTO modprefix_items(itemtype,editable,livedate,removedate,showto,'.
            'title,description,plugininfo,weblink,placeholder,basedonitemid,crosslink,headinglevel,options,rights,data) '.
            'VALUES ('.$this->c($itemtype).','.sql_int($editable).','.
            sql_int($livedate).','.sql_int($removedate).','.$this->c($showto).','.
            $this->c($title).','.$this->c($description).','.$this->c($plugininfo).','.
            $this->c($weblink).','.$this->c($placeholder).','.sql_int($basedonitem).','.
            sql_int($crosslink).','.sql_int($headinglevel).','.$this->c($options).','.sql_int($rights).','.$this->c($data).')');

        return $this->get_serial_number('modprefix_items','id');
    }

    private function get_serial_number($table,$column) {
        global $CFG;
        if($CFG->dbtype=='postgres7') {
            $rs=$this->do_sql(
                'SELECT currval(\''.$table.'_'.$column.'_seq\') AS frog');
            return $rs->fields['frog'];
            // TODO (sam) Implement for MySQL too
        } else {
            throw new Exception("Database type $CFG->dbtype currently unsupported");
        }
    }
*/
        $item = new Object();
        $item->itemtype = self::addslashes_or_null($itemtype);
        $item->editable = $editable;
        $item->livedate = $livedate;
        $item->removedate = $removedate;
        $item->showto = self::addslashes_or_null($showto);

        $item->title = self::addslashes_or_null($title);
        $item->description = self::addslashes_or_null($description);
        $item->plugininfo = self::addslashes_or_null($plugininfo);
        $item->weblink = self::addslashes_or_null($weblink);
        $item->placeholder = self::addslashes_or_null($placeholder);
        $item->basedonitemid = $basedonitem;
        $item->crosslink = $crosslink;
        $item->headinglevel = $headinglevel;
        $item->options = self::addslashes_or_null($options);
        $item->rights = $rights;
        $item->data = self::addslashes_or_null($data);
        return insert_record('resourcepage_items', $item);
    }

    public static function addslashes_or_null($text) {
        if (is_null($text)) {
            return null;
        } else {
            return addslashes($text);
        }
    }

    public function add_item_flag($itemid,$flag,$value=true) {
        $this->do_sql(
            'INSERT INTO modprefix_itemflags(itemid,flag,value) '.
            'VALUES ('.sql_int($itemid).','.$this->c($flag).','.sql_int($value).')');
    }

    /**
     * @param int $itemid Unique item ID
     * @return Array of flags set on this specific item (not including base item);
     *   array is associative flag name => true/false. Flags that aren't set either
     *   way on this item are not included.
     */
    public function get_item_flags($itemid) {
        $rs=$this->do_sql(
            'SELECT flag,value FROM modprefix_itemflags WHERE itemid='.$itemid);
        $result=array();
        while(!$rs->EOF) {
            $result[$rs->fields['flag']]=$rs->fields['value'] ? true : false;
            $rs->MoveNext();
        }
        return $result;
    }

    public function delete_item_flag($itemid,$flag) {
        $this->do_sql(
            'DELETE FROM modprefix_itemflags WHERE itemid='.$itemid.
            'AND flag='.$this->c($flag));
    }

    public function add_item_file($itemid,$filepath,$mimetype,$size) {
        $this->do_sql(
            'INSERT INTO modprefix_files(itemid,filepath,mimetype,size) '.
            'VALUES ('.sql_int($itemid).','.$this->c($filepath).','.
            $this->c($mimetype).','.sql_int($size).')');
    }

    public function delete_item_file($itemid,$filepath) {
        // Delete
        $this->do_sql(
            'DELETE FROM modprefix_files WHERE itemid='.$itemid.
            ' AND filepath='.$this->c($filepath));
        // See if there are any other references (this returns TRUE if
        // there are none left and the file should/can be deleted)
        $rs=$this->do_sql(
            'SELECT COUNT(itemid) AS cnt FROM modprefix_files WHERE filepath='.
            $this->c($filepath));
        return $rs->fields['cnt']==0;
    }


   public function add_container($containertype, $resourcepageid=NULL, $nextupdate=NULL, $plugininfo=NULL, $title=NULL, $livedate=NULL, $removedate=NULL, $showto=NULL) {

		// Use insert_record for database independence
/*
        $rs=$this->do_sql(
            'INSERT INTO modprefix_containers (containertype,nextupdate,plugininfo, resourcepageid, title, livedate, removedate, showto) '.
            'VALUES ('.$this->c($containertype).','.sql_int($nextupdate).','.$this->c($plugininfo)
                      .','.sql_int($resourcepageid).','.$this->c($title).','.sql_int($livedate).','.sql_int($removedate).','.$this->c($showto).')');

        return $this->get_serial_number('modprefix_containers','id');
*/
        $container = new Object();
        $container->containertype = addslashes($containertype);
        $container->nextupdate = $nextupdate;
        $container->plugininfo = addslashes($plugininfo);
        $container->resourcepageid = $resourcepageid;
        $container->title = addslashes($title);
        $container->livedate = $livedate;
        $container->removedate = $removedate;
        $container->showto = addslashes($showto);
        return insert_record('resourcepage_containers', $container);

    }

    public function upd_container($containerid, $containertype, $nextupdate=NULL, $plugininfo=NULL, $title=NULL, $resourcepageid=NULL, $livedate=NULL, $removedate=NULL, $showto=NULL) {

        $set = '';
        $set .= is_null($nextupdate) ? '' : ', nextupdate='.sql_int($nextupdate);
        $set .= is_null($plugininfo) ? '' : ', plugininfo='.$this->c($plugininfo);
        $set .= is_null($title) ? '' : ', title='.$this->c($title);
        $set .= is_null($resourcepageid) ? '' : ', resourcepageid='.sql_int($resourcepageid);
        $set .= is_null($livedate) ? '' : ', livedate='.sql_int($livedate);
        $set .= is_null($removedate) ? '' : ', removedate='.sql_int($removedate);
        $set .= is_null($showto) ? '' : ', showto='.$this->c($showto);

        if (!empty($set)) {
            $rs=$this->do_sql('UPDATE modprefix_containers'.
                              ' SET '.substr($set, 2).
                              ' WHERE id='.sql_int($containerid).
                              ' AND containertype='.$this->c($containertype));
        }
    }

    public function add_container_item($containerid,$itemid,$hidden=false) {
        $this->add_container_thing($containerid,$itemid,'itemid',$hidden);
    }

    public function add_container_subcontainer($containerid,$subcontainerid,$hidden=false) {
        $this->add_container_thing($containerid,$subcontainerid,'subcontainerid',$hidden);
    }

    private function add_container_thing($containerid,$thing,$field,$hidden) {
        // Get new sequence number. Note that this is not concurrency-safe; we could end
        // up with two simultaneous threads picking the same sequence number. The sequencing
        // works the way it does because I wanted to use e.g. 1-5 within an individual container
        // for sequencing, rather than using a shared sequence between all containers. Chance of
        // two people adding item to same container at same time is low.
        $rs=$this->do_sql(
            'SELECT (coalesce(max(sequence),0)+1) AS mx FROM modprefix_containeritems WHERE containerid='.
            sql_int($containerid));
        $sequence=$rs->fields['mx'];
        $this->do_sql(
            'INSERT INTO modprefix_containeritems (containerid,sequence,'.$field.',hidden) '.
            'VALUES ('.sql_int($containerid).",$sequence,".sql_int($thing).','.sql_int($hidden).')');
    }

    public function insert_container_item($containerid,$itemid,$seq,$hidden=false) {
        $this->insert_container_thing($containerid,$itemid,'itemid',$seq,$hidden);
    }

    public function insert_container_subcontainer($containerid,$subcontainerid,$seq,$hidden=false) {
        $this->insert_container_thing($containerid,$subcontainerid,'subcontainerid',$seq,$hidden);
    }

    private function insert_container_thing($containerid,$thing,$field,$seq,$hidden) {
        $tw=new transaction_wrapper();
        // Reason for putting it up 10001 then down 10000 rather than putting it up 1 is that
        // putting it up 1 ends up temporarily violating the primary key constraint.
        $this->do_sql(
            'UPDATE modprefix_containeritems SET sequence=sequence+10001 WHERE sequence>='.$seq.' AND containerid='.$containerid);
        $this->do_sql(
            'UPDATE modprefix_containeritems SET sequence=sequence-10000 WHERE sequence>10000 AND containerid='.$containerid);
        $this->do_sql(
            'INSERT INTO modprefix_containeritems (containerid,sequence,'.$field.',hidden) '.
            'VALUES ('.sql_int($containerid).','.$seq.','.sql_int($thing).','.sql_int($hidden).')');
        $tw->commit();
    }

    /**
     * Sets data fields of an item. Be careful to quote values in the array!
     * @param int $itemid Item ID
     * @param array $changes Associative array of field->quoted string. You must
     *   have already quoted each value for SQL, because this function doesn't
     *   know what type they are
     */
    public function set_item_fields($itemid,&$changes) {
        if(count($changes)==0) {
            return;
        }

        $query='UPDATE modprefix_items SET ';

        $first=true;
        foreach($changes as $field => $quotedvalue) {
            if(!$first) {
                $query.=', ';
            } else {
                $first=false;
            }

            $query.="$field = $quotedvalue";
        }

        $query.=' WHERE id='.sql_int($itemid);

        $this->do_sql($query);
    }

    /**
     * Checks that a container_item is contained within the given top-level
     * containers and is OK for editing (supposing you have edit permission on
     * those containers).
     * @param array $parents Array of top-level parent container IDs
     * @param int $containerid Container in question
     * @param int $itemid Item in question
     */
    public function verify_container_item_editable($parents,$containerid,$itemid) {

        $containercondition='ci1.containerid='.sql_int($parents[0]);
        for($i=1;$i<count($parents);$i++) {
            $containercondition.=' OR ci1.containerid='.sql_int($parents[$i]);
        }

        $query=<<<QUERYENDS
SELECT
  COUNT(ci1.containerid) AS cnt
FROM
  modprefix_containeritems ci1
  LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
  LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
WHERE
  $containercondition AND COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=$itemid
QUERYENDS;

        // TODO (sam) Actually verify that they are allowed to edit the item (it's not within a container
        // that prevents that)

        $rs=$this->do_sql($query);
        if($rs->fields['cnt']==0) {
            error('Specified container item could not be found within the parent container.');
        }
    }

    /**
     * Checks that a container_item is contained within the given top-level
     * containers and is OK for editing (supposing you have edit permission on
     * those containers).
     * @param array $parents Array of top-level parent container IDs
     * @param int $containerid Container in question
     * @param int $subcontainerid Subcontainer in question
     */
    public function verify_container_subcontainer_editable($parents,$containerid,$subcontainerid) {

        $containercondition='ci1.containerid='.sql_int($parents[0]);
        for($i=1;$i<count($parents);$i++) {
            $containercondition.=' OR ci1.containerid='.sql_int($parents[$i]);
        }

        $query=<<<QUERYENDS
SELECT
  COUNT(ci1.containerid) AS cnt
FROM
  modprefix_containeritems ci1
  LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
  LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
WHERE
  $containercondition AND COALESCE(ci1.subcontainerid,ci2.subcontainerid,ci3.subcontainerid)=$subcontainerid
QUERYENDS;

        // TODO (sam) Actually verify that they are allowed to edit the item (it's not within a container
        // that prevents that)

        $rs=$this->do_sql($query);
        if($rs->fields['cnt']==0) {
            error('Specified container item could not be found within the parent container.');
        }
    }

    /**
     * Checks that a container is contained within the given top-level
     * containers and is OK for editing (supposing you have edit permission on
     * those containers).
     * @param array $parents Array of top-level parent container IDs
     * @param int $containerid Container in question
     * @param bool $settingsonly If true, only checks the container itself and not
     *   whether you are allowed to edit things within it
     * @param string $requiredtype If provided, only include containers with the specified
     *   containertype.
     */
    public function verify_container_editable($parents,$containerid,$settingsonly=false,$requiredtype=null) {

        // If the container is one of those top-level ones then don't waste time
        // querying
        if(in_array($containerid,$parents)) {
            return;
        }

        $containercondition='ci1.containerid='.sql_int($parents[0]);
        for($i=1;$i<count($parents);$i++) {
            $containercondition.=' OR ci1.containerid='.sql_int($parents[$i]);
        }

        if($requiredtype) {
            $extracondition='AND c.containertype='.$this->c($requiredtype);
        } else {
            $extracondition='';
        }

        $query=<<<QUERYENDS
SELECT
  COUNT(ci1.containerid) AS cnt
FROM
  modprefix_containeritems ci1
  LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
  LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
  LEFT JOIN modprefix_containers c ON COALESCE(ci3.subcontainerid,ci2.subcontainerid,ci1.subcontainerid)=c.id
WHERE
  $containercondition AND c.id=$containerid
  $extracondition
QUERYENDS;

        if(!$settingsonly) {
            // TODO (sam) Actually verify that they are allowed to edit things in the container
        }

        $rs=$this->do_sql($query);
        if($rs->fields['cnt']==0) {
            error('Specified container item could not be found within the parent container.');
        }
    }

    public function get_item_fields($itemid) {
        $rs=$this->do_sql('SELECT * FROM modprefix_items WHERE id='.$itemid);
        return (object)$rs->FetchRow();
    }

    public function get_container_items() {
        $array=func_get_args();
        return $this->get_container_items_array($array);
    }

    public function get_container_item($containers, $itemid) {
        $containers = $this->get_container_items_array($containers);
        foreach ($containers as $container) {
            $found = $container->get_item_recursive($itemid);
            if (!is_null($found)) {
                return $found;
            }
        }
        throw new Exception('get_container_item - item not found in the container');
    }

    /**
     * Obtains all items within specified containers, and their subcontainers.
     * Intended to obtain all information about those items needed to build an
     * editing or display page containing that container.
     * @param array $containers Array of container IDs
     * @return array Array (same length as $containers) of {@link resourcepage_container}
     *   objects
     */
    public function get_container_items_array($containers) {
        if(count($containers)==0) throw new Exception('get_container_items_array called with no parameters');

        $containercondition='c.id='.sql_int($containers[0]);
        for($i=1;$i<count($containers);$i++) {
            $containercondition.=' OR c.id='.sql_int($containers[$i]);
        }

        // Note that the results of this query are interpreted - and documented - in
        // resourcepage_container.php. Any changes to the query must be reflected in
        // that code and documentation.
        $query=<<<QUERYENDS
SELECT
  ci1.sequence AS seq1,ci2.sequence AS seq2,ci3.sequence AS seq3,
  c.id AS container1,ci1.subcontainerid AS container2,ci2.subcontainerid AS container3,
  '1: item' AS rowtype,
  ci1.hidden AS hidden1,ci2.hidden AS hidden2,ci3.hidden AS hidden3,
  c.plugininfo AS plugininfo1,c2.plugininfo AS plugininfo2,c3.plugininfo AS plugininfo3,
  c.containertype AS containertype1,c2.containertype AS containertype2,c3.containertype AS containertype3,
  c.nextupdate AS nextupdate1,c2.nextupdate AS nextupdate2,c3.nextupdate AS nextupdate3,
  c.title AS title1, c2.title AS title2,c3.title AS title3,
  c.resourcepageid AS resourcepageid1, c2.resourcepageid AS resourcepageid2,c3.resourcepageid AS resourcepageid3,
  c.livedate AS livedate1, c2.livedate AS livedate2,c3.livedate AS livedate3,
  c.removedate AS removedate1, c2.removedate AS removedate2,c3.removedate AS removedate3,
  c.showto AS showto1, c2.showto AS showto2,c3.showto AS showto3,
  i.id AS item,
  i.basedonitemid AS basedonitem,
  i.itemtype,
  bi.itemtype AS baseitemtype,
  i.editable,
  bi.editable AS baseeditable,
  i.livedate,
  bi.livedate AS baselivedate,
  i.removedate,
  bi.removedate AS baseremovedate,
  i.showto,
  bi.showto AS baseshowto,
  i.title,
  bi.title AS basetitle,
  i.description,
  bi.description AS basedescription,
  i.plugininfo,
  bi.plugininfo AS baseplugininfo,
  i.forcehide,
  bi.forcehide AS baseforcehide,
  i.weblink,
  bi.weblink AS baseweblink,
  i.placeholder,
  i.crosslink,
  bi.crosslink AS basecrosslink,
  i.headinglevel,
  bi.headinglevel AS baseheadinglevel,
  rp.name AS crosslinktitle,
  NULL as flag,NULL as value,
  NULL as filepath,NULL as mimetype,CAST(NULL AS SIGNED) as "size",CAST(NULL AS SIGNED) as basefile,
  i.options,
  bi.options AS baseoptions,
  i.rights,
  bi.rights AS baserights,
  i.data,
  bi.data AS basedata
FROM
  modprefix_containers c
  LEFT OUTER JOIN modprefix_containeritems ci1 ON c.id=ci1.containerid
  LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
  LEFT OUTER JOIN modprefix_containers c2 ON ci1.subcontainerid=c2.id
  LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
  LEFT OUTER JOIN modprefix_containers c3 ON ci2.subcontainerid=c3.id
  LEFT OUTER JOIN modprefix_items i ON COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=i.id
  LEFT OUTER JOIN modprefix_items bi ON i.basedonitemid=bi.id
  LEFT OUTER JOIN prefix_resourcepage rp ON COALESCE(i.crosslink,bi.crosslink)=rp.id
WHERE
  $containercondition
UNION ALL
SELECT
  ci1.sequence,ci2.sequence,ci3.sequence,
  c.id,ci2.containerid,ci3.containerid,
  '2: flag',
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  fl.flag,fl.value,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL
  ,NULL,NULL
FROM
  modprefix_containers c
  LEFT OUTER JOIN modprefix_containeritems ci1 ON c.id=ci1.containerid
  LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
  LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
  INNER JOIN modprefix_itemflags fl ON COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=fl.itemid
WHERE
  $containercondition
UNION ALL
SELECT
  ci1.sequence,ci2.sequence,ci3.sequence,
  c.id,ci2.containerid,ci3.containerid,
  '3: baseflag',
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  fl.flag,fl.value,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL
  ,NULL,NULL
FROM
  modprefix_containers c
  LEFT OUTER JOIN modprefix_containeritems ci1 ON c.id=ci1.containerid
  LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
  LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
  INNER JOIN modprefix_items i ON COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=i.id
  INNER JOIN modprefix_items bi ON i.basedonitemid=bi.id
  INNER JOIN modprefix_itemflags fl ON bi.id=fl.itemid
WHERE
  $containercondition
UNION ALL
SELECT
  ci1.sequence,ci2.sequence,ci3.sequence,
  c.id,ci2.containerid,ci3.containerid,
  '4: file',
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,
  NULL,NULL,fi.filepath,fi.mimetype,fi.size,CASE WHEN fi.itemid=bi.id THEN 1 ELSE 0 END,NULL,NULL,NULL,NULL
  ,NULL,NULL
FROM
  modprefix_containers c
  LEFT OUTER JOIN modprefix_containeritems ci1 ON c.id=ci1.containerid
  LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
  LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
  INNER JOIN modprefix_items i ON COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=i.id
  LEFT OUTER JOIN modprefix_items bi ON i.basedonitemid=bi.id
  INNER JOIN modprefix_files fi ON (COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=fi.itemid OR bi.id=fi.itemid)
WHERE
  $containercondition
ORDER BY
  4,1,2,3,7,62
QUERYENDS;

        // Actually run query
        $rs=$this->do_sql($query);

        // Process results
        while(!$rs->EOF) {

            // Check which container current row is for
            $found=false;
            for($i=0;$i<count($containers);$i++) {
                // When we find a matching container, create its
                // result field and let it handle the resultset
                // until it runs out
                if($rs->fields['container1']==$containers[$i]) {
                    $result[$i]=new resourcepage_container();
                    $result[$i]->init_from_database($rs);
                    $found=true;
                    break;
                }
            }
            if(!$found) {
                print '<pre>';
                print_r ($rs->fields);
                print '</pre>';
                throw new Exception('Obtained results for wrong container (query bug?)');
            }
        }

        // Fill in any empty containers
        for($i=0;$i<count($containers);$i++) {
            if(!isset($result[$i]) || is_null($result[$i])) {
                print 'Setting not null!';
                $result[$i]=new resourcepage_container();
            }
        }

        return $result;
    }

    /**
     * Carries out steps necessary to add a pull extension to a page.
     * @param object $extobj Extension class object
     * @param int $parentcontainer Parent container to add it to (e.g. 'containerid'
     *   from resourcepage table entry)
     * @param object $form Form input fields.
     */

   function add_pull_extension($resourcepageid, $extobj,$parentcontainer,$form) {
        $containertype='pull_'.$extobj->get_name();

        $tw=new transaction_wrapper();

        // Add container and add it into resource page
        // need to include an owner resource page id

        /*
         * The additional chanhe checks if processing is for the rssfeed, and if so...
         * will check if the RSS feed settings have already been specified by otehr users.
         * If an RSS feed exists in database, then the existing RSS feed is added to the container,
         * otherwise a new RSS feed is created.
         *
         * This check ensures only 1 copy of RSS feeds and settings exists and is reused in all
         * resourcepage containers.
         *
         */
        $rssfeed = FALSE;
        if ($form->extension == 'rssfeed') {
            $stringToHash = $form->word.'::'.$form->numberoflinks.'::'.$form->feedupdates.'::'.$form->includetitle.'::';
            $hashString   = md5($stringToHash);

            /*
             * All this code checks for an existing RSS feed that matches the requesting RSS feed settings
             * If found, rssfeed will not be FALSE.
             *
             */
            $word = trim(clean_param($form->word, PARAM_URL));
            if (($word !== '') && is_numeric($form->numberoflinks) && is_numeric($form->feedupdates) && is_numeric($form->includetitle)) {
                global $CFG;

                $sql = <<<EOF
SELECT *
FROM   {$CFG->prefix}resourcepage_x_rssfeed
WHERE  word          = '$word'
AND    numberoflinks = $form->numberoflinks
AND    feedupdates   = $form->feedupdates
AND    includetitle  = $form->includetitle
LIMIT  1
EOF;

                $rssfeed = get_records_sql($sql);
                if ($rssfeed !== FALSE) {
                    foreach ($rssfeed as $feed) {
                        $containerid = $feed->containerid;
                        if ($containerid <= 0) {
                            $rssfeed = FALSE;
                        }
                    }
                }
            }
        }
        if ($rssfeed !== FALSE) {

            /*
             * If found, then add the found RSS feed to the container and do not
             * create a new RSS feed.
             *
             */
            $this->add_container_subcontainer($parentcontainer,$containerid);

            try {
                $tw->commit();
            } catch(Exception $e) {
                $tw->rollback();
                throw $e;
            }

        } else {

            /*
             * If not rssfeed extension or if no existing RSS feed is found,
             * then perform normal operation and create the extension and add to
             * container....
             *
             */
            $containerid=$this->add_container($containertype, $resourcepageid);
            $this->add_container_subcontainer($parentcontainer,$containerid);

            // Add new instance
            $form->containerid=$containerid;
            $extobj->add_instance($form);

            // Do first update

            try {
                $this->update_extension($containerid,$extobj,null);
                $tw->commit();
            } catch(Exception $e) {
                $tw->rollback();
                throw $e;
            }

        }

        /*
         * The return is container id is used, so do not remove this return value.
         * Specifically it is used by the rssfeed extension
         *
         */
        return $containerid;

    }

    /**
     * Adds a canned item to a particular resourcepage. Adds to the bottom of the
     * selected position list.
     * @param int $resourcepageid ID of page
     * @param string $canned ID of item
     * @param int $position Position (0=top, 1=bottom)
     */
    public function add_canned_usage($resourcepageid,$canned,$position) {
        $tw=new transaction_wrapper();
        $rs=$this->do_sql(<<<ENDQUERY
SELECT
  MAX(sequence) AS max
FROM
  modprefix_cannedusage cu
WHERE
  cu.resourcepageid=$resourcepageid AND cu.position='$position'
ENDQUERY
            );
        if($max=$rs->fields['max']) {
            $sequence=$max+1;
        } else {
            $sequence=1;
        }

        $this->do_sql(<<<ENDQUERY
INSERT INTO modprefix_cannedusage(
  resourcepageid,canned,position,sequence)
VALUES (
  $resourcepageid,'$canned','$position',$sequence)
ENDQUERY
            );

        $tw->commit();
    }

    /**
     * Deletes a canned item from a particular resourcepage.
     * @param int $resourcepageid ID of page
     * @param string $canned ID of item
     */
    public function delete_canned_usage($resourcepageid,$canned) {
        $this->do_sql(<<<ENDQUERY
DELETE FROM
  modprefix_cannedusage
WHERE
  resourcepageid=$resourcepageid AND canned='$canned'
ENDQUERY
            );
    }

    /**
     * Obtains information about canned text items, either in general
     * or for a specific resourcepage.
     * @param int $resourcepageid ID of resource page, leave zero to
     *   get general list of canned items
     * @return object An object containing three arrays, 'unused',
     *   'top', and 'bottom', each of which is an ordered list of
     *   canned item (resourcepage_canned) objects.
     */
    public function get_canned_info($resourcepageid=0) {
        if($resourcepageid) {
            // Get list of all canned items, and details about those
            // used on this page
/*
 In following query replaced c.html by
*/
            $rs=$this->do_sql(<<<ENDQUERY
SELECT
  c.canned AS canned,
  cast(c.html as CHAR(255)) as html,
  c.displayname,
  cu.position AS position,
  cu.sequence AS sequence
FROM
  modprefix_canned c
  LEFT OUTER JOIN modprefix_cannedusage cu ON cu.canned=c.canned
WHERE
 cu.resourcepageid=$resourcepageid
UNION
SELECT
  c.canned,
  cast(c.html as CHAR(255)) as html,
  c.displayname,
  NULL,
  NULL
FROM
  modprefix_canned c
WHERE
  NOT (c.canned IN (SELECT canned FROM modprefix_cannedusage WHERE resourcepageid=$resourcepageid))
ORDER BY
  position,sequence,canned
ENDQUERY
                );
        } else {
            $rs=$this->do_sql(<<<ENDQUERY
SELECT
  c.canned AS canned,
  c.html,
  c.displayname,
  NULL as position,
  NULL as sequence
FROM
  modprefix_canned c
ORDER BY
  c.canned
ENDQUERY
            );
        }

        $result=new stdClass;
        $result->unused=array();
        $result->top=array();
        $result->bottom=array();
        while(!$rs->EOF) {
            // Build information about this canned item
            $entry=new resourcepage_canned(
                $rs->fields['canned'],
                $rs->fields['html'],
                $rs->fields['displayname']);

            $position=$rs->fields['position'];
            if(is_null($position)) {
                $result->unused[]=$entry;
            } else if($position==0) {
                $result->top[]=$entry;
            } else if($position==1) {
                $result->bottom[]=$entry;
            } else {
                throw new Exception('Unexpected position');
            }

            $rs->MoveNext();
        }

        return $result;
    }

    public function c($string) {
        return sql_char($string);
    }

    /**
     * Obtains a single extension without checking for database upgrades.
     * Throws an exception if extension doesn't exist.
     * @return resourcepage_extension Specified extension object
     */
    public function get_specified_extension($name) {

        $extensionsfolder=dirname(__FILE__).'/extensions';
        $phpname="$extensionsfolder/$name/$name.php";
        if(!is_file($phpname)) {
            throw new Exception("Expected extension $name not found");
        }
        require_once($phpname);
        $classname='resourcepage_'.$name;
        return new $classname($this);
    }

    /**
     * Lists all extensions that are found (without checking whether they
     * might need database updates)
     * @return array Associative array of extension name => extension object
     */
    public function get_extensions() {

        // Scan extensions folder to obtain list of objects
        $extensions=array();
        $extensionsfolder=dirname(__FILE__).'/extensions';
        if(!$dh=opendir($extensionsfolder)) {
            throw new Exception('Failed to scan extensions folder');
        }
        while (false !== ($filename = readdir($dh))) {
            $phpname="$extensionsfolder/$filename/$filename.php";
            if(preg_match('/[a-z]+/',$filename) && is_file($phpname)) {
                // OK, valid extension
                require_once($phpname);
                $classname='resourcepage_'.$filename;
                $extensions[$filename]=new $classname($this);
            }
        }

        return $extensions;
    }

    public function uninstall_extension($extensionobj) {
        $tw=new transaction_wrapper();
        $extensionobj->uninstall();
        $this->do_sql(
            'DELETE FROM modprefix_extensions '.
            'WHERE extension='.$this->c($extensionobj->get_name()));
        $tw->commit();
    }

    /**
     * Creates the list of extensions based on the currently-present files
     * and makes database upgrade calls if necessary.
     * @return array Associative array of extension name => extension object
     */
    public function create_extensions_list() {

        global $CFG;
        $extensions=$this->get_extensions();

        // Get database table of known extensions as associative array
        $rs=$this->do_sql(
            'SELECT extension,version FROM '.$CFG->prefix.'resourcepage_extensions');
        $dbextensions=$rs->GetAssoc();

        // Do we have any in the list that aren't in the database?
        foreach($extensions as $extension) {
            if(!array_key_exists($extension->get_name(),$dbextensions)) {
                // Install
                $tw=new transaction_wrapper();
                $this->do_sql(
                    'INSERT INTO modprefix_extensions(extension,version) '.
                    'VALUES ('.$this->c($extension->get_name()).','.
                    $this->c($extension->get_database_version()).')');
                $extension->install();
                $tw->commit();
            } else {
                $dbversion=$dbextensions[$extension->get_name()];
                if($dbversion < $extension->get_database_version()) {
                    // Update
                    $tw=new transaction_wrapper();

                    // Need to run XMLDB upgrade script now
                    $upgradefilename = $extension->get_upgrade_filename();
                    if (file_exists($upgradefilename)) {
                        include_once($upgradefilename);
                        $upgradefunction = 'xmldb_'.$extension->get_name().'_upgrade';
                        $upgradefunction($dbversion);
                    }

                    $this->do_sql(
                        'UPDATE modprefix_extensions SET version='.
                        $this->c($extension->get_database_version()).
                        ' WHERE extension='.$this->c($extension->get_name()));
                    $tw->commit();
                }
            }
        }

        // Note: We don't uninstall or delete entries that refer to extensions that
        // are no longer present but have stuff in database. Since the code's not there
        // we can't call uninstall and anyway the data might be wanted later, maybe it
        // was only removed temporarily. There should later be a manual way to uninstall
        // resourcepage extensions from the resourcepage global settings page under admin.

        return $extensions;
    }

    public function get_cron_frequency() {
        // Request cron every 10 minutes
        return 10*60;
    }

    /**
     * In cron, goes through the containers seeing if any need updating.
     * @return bool True if cron was successful.
     */
    public function cron() {
        error_reporting(E_ALL);
        $extensions=$this->create_extensions_list();

        // Get list of extensions and see if any need cronning
        mtrace('Listing extensions...','');
        $rs=$this->do_sql(
            'SELECT extension,lastcron FROM modprefix_extensions');
        $dbextensions=$rs->GetAssoc();
        mtrace('checking each...');
        foreach($dbextensions as $name=>$lastcron) {
            $extension =& $extensions[$name];
            if(!$extension) {
                // Extension been deleted? Ignore for now
                continue;
            }

            $freq=$extension->get_cron_frequency();
            if($freq && (is_null($lastcron) || time()-$lastcron >= $freq)) {
                try {
                    mtrace('Running extension: '.$name.'...');
                    // Update database
                    $this->do_sql(
                        'UPDATE modprefix_extensions SET lastcron='.time().
                        ' WHERE extension='.$this->c($name));
                    // Run cron
                    $now=microtime(true);
                    $extension->cron();
                    mtrace($name.' complete ('.round(microtime(true)-$now,1).'s)');
                } catch(Exception $e) {
                    mtrace('Exception when updating push extension '.
                        $name.': '.$e->getMessage().' -- '.$e->getFile().':'.$e->getLine()."\n");
                }
            }
            unset($extension);
        }

        // Update a limited number (max 100) of pull extension containers
        mtrace('Listing containers...','');
        global $CFG;
        $updatelimit=empty($CFG->resourcepagecronlimit)
            ? '100'
            : $CFG->resourcepagecronlimit;
        $rs=$this->do_sql(
            'SELECT id,containertype,nextupdate,plugininfo FROM '.
            'modprefix_containers '.
            'WHERE nextupdate IS NOT NULL AND nextupdate < '.time().
            ' ORDER BY nextupdate LIMIT '.$updatelimit);

        $rows=array();
        while($row=$rs->FetchNextObject()) {
            $rows[]=$row;
        }
        $containernow=microtime(true);
        mtrace(count($rows).' found, checking each...');
        foreach($rows as $row) {
            $matches=array(); // Just to stop warning
            if(preg_match(self::CONTAINERTYPE_PULLEXTENSION,$row->CONTAINERTYPE,$matches)) {
                // Get extension object
                $extension=$extensions[$matches[1]];
                if(!$extension) { // Extension was uninstalled or something
                    continue;
                }

                try {
                    mtrace('Updating container '.$row->ID.' (',$row->CONTAINERTYPE.')...');
                    $now=microtime(true);
                    $this->update_extension($row->ID,$extension,$row->PLUGININFO);
                    mtrace($row->ID.' complete ('.round(microtime(true)-$now,1).'s)');
                } catch(Exception $e) {
                    mtrace('Exception when updating extension container #'.
                        $row->ID.': '.$e->getMessage().' -- '.$e->getFile().':'.$e->getLine()."\n");
                }
            } else {
                mtrace('Unexpected container returned for cron check: '.$row->ID);
            }
        }
        mtrace('Containers complete ('.round(microtime(true)-$containernow,1).'s)');

        // Find any rpfilelinks that were automatically hidden, but where a file is now present,
        // and make them visible.
        mtrace('Updating rpfilelinks started '.date('H:i:s').'... ', '');
        $coursechanged = array();
        $sql = "
        SELECT cm.id AS cmid, res.id, cm.course AS courseid
        FROM {$CFG->prefix}resource res
        JOIN {$CFG->prefix}course_modules cm      ON cm.instance = res.id
        JOIN {$CFG->prefix}modules m              ON m.id = cm.module AND m.name = 'resource'
        JOIN {$CFG->prefix}resourcepage_items rpi ON rpi.id = CAST(res.reference AS integer)
        LEFT JOIN {$CFG->prefix}resourcepage_items rpbi ON rpbi.id = rpi.basedonitemid
        JOIN {$CFG->prefix}resourcepage_files rpf ON (rpf.itemid = rpi.id OR rpf.itemid = rpbi.id)
        WHERE
            res.type = 'rpfilelink'
        AND res.alltext = 'autohidden'
        AND cm.visible = 0
        AND COALESCE(rpi.rights, rpbi.rights) = 1";
        $rpfilelinkstounhide = get_records_sql($sql);
        if ($rpfilelinkstounhide) {
            foreach ($rpfilelinkstounhide as $resource) {
                set_coursemodule_visible($resource->cmid, 1);
                set_field('resource', 'alltext', '', 'id', $resource->id);
                $coursechanged[$resource->courseid] = 1;
            }
        }

        // Find any rpfilelinks where there is no longer a file attached, and automatically set
        // them to be hidden.
        $sql = "
        SELECT cm.id AS cmid, res.id, cm.course AS courseid
        FROM {$CFG->prefix}resource res
        JOIN {$CFG->prefix}course_modules cm      ON cm.instance = res.id
        JOIN {$CFG->prefix}modules m              ON m.id = cm.module AND m.name = 'resource'
        JOIN {$CFG->prefix}resourcepage_items rpi ON rpi.id = CAST(res.reference AS integer)
        LEFT JOIN {$CFG->prefix}resourcepage_items rpbi ON rpbi.id = rpi.basedonitemid
        LEFT JOIN {$CFG->prefix}resourcepage_files rpf ON (rpf.itemid = rpi.id OR rpf.itemid = rpbi.id)
        WHERE
            (rpf.filepath IS NULL OR COALESCE(rpi.rights, rpbi.rights) IS NULL OR COALESCE(rpi.rights, rpbi.rights) = 0)
        AND res.type = 'rpfilelink'
        AND cm.visible = 1";
        $rpfilelinkstohide = get_records_sql($sql);
        if ($rpfilelinkstohide) {
            foreach ($rpfilelinkstohide as $resource) {
                set_coursemodule_visible($resource->cmid, 0);
                set_field('resource', 'alltext', 'autohidden', 'id', $resource->id);
                $coursechanged[$resource->courseid] = 1;
            }
        }
        foreach ($coursechanged as $courseid => $notused) {
            rebuild_course_cache($courseid);
        }

        return true;
    }

    const CONTAINERTYPE_PULLEXTENSION='/^pull_([a-z]+)$/';

    /**
     * Convert a date in 'days since course start' to absolute date.
     * @param int $coursestart Course start date (seconds since 1970)
     * @param int $days Positive or negative days since course start
     */
    public static function convert_date($coursestart,$days) {
        if(is_null($days)) {
            $days=0;
        }
        $sign=$days>=0?'+':'';
        return strtotime("$sign$days days",$coursestart);
    }

    /**
     * Updates a single extension instance and sets the database table appropriately.
     * @param int $container Container ID
     * @param resourcepage_extension_pull $extension Extension object being updated
     * @param string $plugininfobefore Previous version of plugininfo
     * @throws Exception If something goes wrong with update
     */
    public function update_extension($container,$extension,$plugininfobefore) {
        // Set defaults
        $nextupdate=strtotime('+1 day');
        $plugininfo=$plugininfobefore;

        // TODO sam - remove this nasty RSS-feed-specific bit and put it into the
        // rssfeed code instead.
        // need to set rss feed check only to false
        if (method_exists($extension, 'set_checkonly')) {
            $extension->set_checkonly(false);
        }

        // Run update
        $tw=new transaction_wrapper();
        try {
            $extension->update($container,$nextupdate,$plugininfo);
            $tw->commit();
        } catch(Exception $e) {
            $tw->rollback();
            // Only throw exception if not running cron
            if (!defined('FULLME') || FULLME !== 'cron') {
                throw $e;
            }
        }

        // $nextupdate is supposed to be *either* a timestamp, *or* zero if no update
        // is needed
        if($nextupdate==0) {
            $nextupdate='NULL';
        }
        $tl = textlib_get_instance();
        $this->do_sql(
            'UPDATE modprefix_containers SET '.
                'nextupdate='.$nextupdate.
                ($plugininfo===$plugininfobefore
                    ? ''
                    : ',plugininfo='.$this->c($tl->substr($plugininfo,0,255))).
                ' WHERE id='.$container);
      }

    public function process_file_request($courseid,$args) {
        global $CFG;
        // If they're currently editing the course, allow them to have the
        // file even if it's restricted.
        // [Note that there might be a security hole in isediting that allows
        // any teacher on the course to get access to all files, especially if
        // they have editing rights on some other course]
        $getoutofjailfree=isediting($courseid);

        // Check if it's a deleted request
        if($args[0]=='!deleted') {
            if(!$getoutofjailfree) {
                error('This file can only be accessed while editing the course');
            }
            return $CFG->dataroot.'/'.$courseid.'/moddata/resourcepage/!deleted/'.$args[1];
        }


        // Arguments:
        // * Item ID
        // * Actual filepath

        $itemid=(int)$args[0];

        $filepath=implode('/',array_slice($args,1));
        $quotedfilepath=$this->c($filepath);

        if(isset($CFG->ousite)) {
            $ouspecificcmbit='cm.availablefrom, cm.availableuntil, cm.showto AS moduleshowto';
        } else {
            $ouspecificcmbit='0 AS availablefrom, 0 AS availableuntil, NULL as moduleshowto';
        }
        $query=<<<QUERYENDS
SELECT
  CASE WHEN rp.deletedcontainerid=ci1.containerid THEN 1 ELSE 0 END AS deleted,
  ci3.hidden AS hidden3, c3.livedate AS livedate3, c3.removedate AS removedate3, c3.showto AS showto3,
  ci2.hidden AS hidden2, c2.livedate AS livedate2, c2.removedate AS removedate2, c2.showto AS showto2,
  ci1.hidden AS hidden1, c1.livedate AS livedate1, c1.removedate AS removedate1, c1.showto AS showto1,
  i.livedate AS livedate0, i.removedate AS removedate0,i.showto AS showto0,
  coalesce(i.forcehide,bi.forcehide,0) AS forcehide, c.startdate AS coursestart,
  $ouspecificcmbit
FROM
  prefix_resourcepage rp
  INNER JOIN modprefix_items i ON i.id=$itemid
  LEFT JOIN modprefix_items bi ON i.basedonitemid=bi.id
  INNER JOIN modprefix_files f ON f.itemid=$itemid OR f.itemid=bi.id
  INNER JOIN modprefix_containeritems ci1 ON rp.containerid=ci1.containerid OR rp.deletedcontainerid=ci1.containerid
  INNER JOIN modprefix_containers c1 ON c1.id = ci1.containerid
  LEFT JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
  LEFT JOIN modprefix_containers c2 ON c2.id = ci2.containerid
  LEFT JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
  LEFT JOIN modprefix_containers c3 ON c3.id = ci3.containerid
  INNER JOIN prefix_course_modules cm ON rp.id=cm.instance AND cm.module=(SELECT id from prefix_modules where name='resourcepage')
  INNER JOIN prefix_course c ON rp.course = c.id
WHERE
  rp.course=$courseid AND cm.course=$courseid AND
  COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=$itemid AND
  f.filepath=$quotedfilepath
QUERYENDS;
        $rs=$this->do_sql($query);
        if($rs->EOF) {
            error('File not found or not accessible');
        }

        while($row = rs_fetch_next_record($rs)) {
            $failmessage = '';

            if($row->deleted) {
                $failmessage='File has been deleted';
                continue;
            }

            if((bool)($row->forcehide)) {
                $failmessage='File is unavailable';
                continue;
            }

            // Item and container restrictions
            $startdate = $row->coursestart;
            for($i = 0; $i <= 3; $i++) {
                // Item and container live/remove dates
                $livefield = 'livedate' . $i;
                $removefield = 'removedate' . $i;
                $livedate = $row->{$livefield};
                $removedate = $row->{$removefield};
                if((!is_null($livedate) && time() < self::convert_date($startdate,$livedate)) ||
                    (!is_null($removedate) && time() >= self::convert_date($startdate,$removedate))) {
                    if(!$getoutofjailfree) {
                        $failmessage='File is currently unavailable';
                        break;
                    }
                }

                // Item and container showto
                $showtofield = 'showto' . $i;
                $showto = $row->{$showtofield};
                if (defined('AUTH_SAMS') && AUTH_SAMS == $CFG->auth && 
                    !SamsAuth::$inst->match_authids($showto)) {
                    $failmessage = 'You do not have permission to access this file';
                    break;
                }

                // Container hidden state
                if($i>0) {
                    $hiddenfield = 'hidden' . $i;
                    $hidden = $row->{$hiddenfield};
                    if ($hidden) {
                        $failmessage = 'File is hidden';
                        break;
                    }
                }
            }
            if($failmessage) {
                continue;
            }

            if((!empty($row->availablefrom) && time() < $row->availablefrom) ||
               (!empty($row->availableuntil) && time() >= $row->availableuntil)) {
                if(!$getoutofjailfree) {
                    $failmessage = 'File is currently unavailable';
                    continue;
                }
            }

            // Check the showto permissions (item and module) if we have SAMS data
            if (defined('AUTH_SAMS') && AUTH_SAMS == $CFG->auth &&
                $row->moduleshowto && !SamsAuth::$inst->match_authids($row->moduleshowto) &&
                !$getoutofjailfree) {
                $failmessage = 'You do not have permission to access this file';
                continue;
            }
            
            if(!$failmessage) { // Hooray! No problems, use this row
                break;
            }
        }
        if($failmessage) {
            error($failmessage);
        }
        rs_close($rs);

        // OK they are allowed to have the file! Give the path
        return $CFG->dataroot.'/'.$filepath;
    }

    public function delete_course($course) {
        // Tell extensions to delete any references
        foreach($this->get_extensions() as $extension) {
            $extension->before_deleting_course($course->id);
        }
    }

    private function backup_cannedarray(&$backup,$tag,$array) {
        $backup->tag_start($tag);
        foreach($array as $canneditem) {
            $backup->tag_start('ITEM');

            $backup->tag_full('CANNED',$canneditem->get_canned(), false);
            $backup->tag_full('DISPLAYNAME',$canneditem->get_display_name(), false);
            $backup->tag_full('HTML',$canneditem->get_html(), false);

            $backup->tag_end('ITEM');
        }
        $backup->tag_end($tag);
    }

    /**
     * @param string $canned String ID of canned text item
     * @return True if the canned ID exists in database
     */
    public function has_canned($canned) {
        $rs=$this->do_sql(
            "SELECT COUNT(canned) AS count
            FROM modprefix_canned
            WHERE canned='$canned'");
        return $rs->fields['count']==1;
    }

    /**
     * @param string $canned String ID of new canned text item
     * @param string $displayname Name displayed to staff on add button
     * @param string $html HTML actually displayed to students.
     */
    public function add_canned($canned,$displayname,$html) {
        $this->do_sql(
            'INSERT INTO modprefix_canned(canned,displayname,html)
            VALUES('.$this->c($canned).','.$this->c($displayname).','.
            $this->c($html).')');
    }

    private function restore_canned($resourcepageid,$position,$xml) {
        // Cache which canned items exist, just to save time
        static $cannedexistence=array();

        $items=isset($xml['ITEM']) ? $xml['ITEM'] : array();
        foreach($items as $itemhash) {
            $item=$itemhash['#'];
            $canned=$item['CANNED'][0]['#'];

            // Check canned item itself exists in this database...
            if(empty($cannedexistence[$canned])) {
                if(!$this->has_canned($canned)) {
                    $this->add_canned($canned,
                        $item['DISPLAYNAME'][0]['#'],
                        $item['HTML'][0]['#']);
                }
                $cannedexistence[$canned]=true;
            }

            $this->add_canned_usage($resourcepageid,$canned,$position);
        }

    }

    public function backup($backup,$mainrow,$userdata,$sitefiles) {
        // Canned text
        $backup->tag_start('CANNED');

        $canned=$this->get_canned_info($mainrow->id);
        $this->backup_cannedarray($backup,'TOP',$canned->top);
        $this->backup_cannedarray($backup,'BOTTOM',$canned->bottom);

        $backup->tag_end('CANNED');

        // Containers
        $containers=$this->get_container_items($mainrow->containerid,$mainrow->deletedcontainerid);

        $backup->tag_start('MAIN');
        $containers[0]->backup($backup,$this,$mainrow,$sitefiles);
        $backup->tag_end('MAIN');

        $backup->tag_start('DELETED');
        $containers[1]->backup($backup,$this,$mainrow,$sitefiles);
        $backup->tag_end('DELETED');
    }

    public function restore($restoresettings,$xml,$userdata) {
        global $SESSION;
        $tw=new transaction_wrapper();

        // Create new instance
        $content=new stdClass;
        $content->name=addslashes($xml['NAME'][0]['#']);
        $content->shared=isset($xml['SHARED'][0]['#']) ? $xml['SHARED'][0]['#'] : 0;
        $content->publicfeed=isset($xml['PUBLICFEED'][0]['#']) ? $xml['PUBLICFEED'][0]['#'] : 0;
        $content->refstyle=isset($xml['REFSTYLE'][0]['#']) ? $xml['REFSTYLE'][0]['#'] : 0;
        $content->exportrefs=isset($xml['EXPORTREFS'][0]['#']) ? $xml['EXPORTREFS'][0]['#'] : 0;
        $content->course=$restoresettings->course_id;
        $newid=$this->add_instance($content);

        // Store resourcepage id for setting up resourcepage containers
        $restoresettings->resourcepageid = $newid;

        if(array_key_exists('DBVERSION',$xml)) {
            $restoresettings->dbversion = $xml['DBVERSION'][0]['#'];
        }

        resourcepage_container::restore(
            $this,$restoresettings,0,
            $xml['MAIN'][0]['#']['THING'][0]['#'],$content->containerid);
        resourcepage_container::restore(
            $this,$restoresettings,0,
            $xml['DELETED'][0]['#']['THING'][0]['#'],$content->deletedcontainerid);

        $this->restore_canned($newid,0,$xml['CANNED'][0]['#']['TOP'][0]['#']);
        $this->restore_canned($newid,1,$xml['CANNED'][0]['#']['BOTTOM'][0]['#']);

        if (!empty($xml['PARENTRESOURCEPAGEID'][0]['#'])) {
            // If this is an old backup file with parent information stored in the resourcepage
            // module, save it for later transfer to the course_modules table.
            // What we are storing here is the newid of the child pointing to the oldid of the parent.
            $SESSION->resourcepagerestoreparents[$newid] = $xml['PARENTRESOURCEPAGEID'][0]['#'];
        }

        $tw->commit();
        return $newid;
    }

    public function restore_decode_links($restoresettings) {
        $tw=new transaction_wrapper();

        // Get all the items that might have links in, from the relevant new course
        $rs=$this->do_sql("
SELECT
    i.id,i.weblink,i.description
FROM
    prefix_resourcepage rp
    INNER JOIN modprefix_containeritems ci1 ON ci1.containerid=rp.containerid OR ci1.containerid=rp.deletedcontainerid
    LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
    LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
    LEFT OUTER JOIN modprefix_items i ON COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=i.id
WHERE
    rp.course={$restoresettings->course_id}
    AND (i.weblink IS NOT NULL OR i.description IS NOT NULL)");
        while(!$rs->EOF) {
            $newweblink=null;
            $newdescription=null;
            if($rs->fields['weblink']) {
                $newweblink=restore_decode_content_links_worker($rs->fields['weblink'],$restoresettings);
                if($newweblink==$rs->fields['weblink']) {
                    $newweblink=null;
                }
            }
            if($rs->fields['description']) {
                $newdescription=restore_decode_content_links_worker($rs->fields['description'],$restoresettings);
                if($newdescription==$rs->fields['description']) {
                    $newdescription=null;
                }
            }
            if($newweblink && $newdescription) {
                $this->do_sql('UPDATE modprefix_items SET weblink='.$this->c($newweblink).
                    ', description='.$this->c($newdescription).' WHERE id='.$rs->fields['id']);
            } else if($newweblink) {
                $this->do_sql('UPDATE modprefix_items SET weblink='.$this->c($newweblink).
                    ' WHERE id='.$rs->fields['id']);
            } else if($newdescription) {
                $this->do_sql('UPDATE modprefix_items SET description='.$this->c($newdescription).
                    ' WHERE id='.$rs->fields['id']);
            }

            $rs->MoveNext();
        }

        // Now change any crosslinks...
        // Note: Original crosslink stored in plugininfo to avoid FK constraints
        //       by resourcepage_item restore() function
        $rs=$this->do_sql("
SELECT
    i.id,i.plugininfo, cm.id AS cmid
FROM
    prefix_resourcepage rp
    INNER JOIN prefix_course_modules cm ON cm.instance=rp.id
    INNER JOIN prefix_modules m ON m.id=cm.module
    INNER JOIN modprefix_containeritems ci1 ON ci1.containerid=rp.containerid OR ci1.containerid=rp.deletedcontainerid
    LEFT OUTER JOIN modprefix_containeritems ci2 ON ci1.subcontainerid=ci2.containerid AND ci1.subcontainerid IS NOT NULL
    LEFT OUTER JOIN modprefix_containeritems ci3 ON ci2.subcontainerid=ci3.containerid
    LEFT OUTER JOIN modprefix_items i ON COALESCE(ci1.itemid,ci2.itemid,ci3.itemid)=i.id
WHERE
    rp.course={$restoresettings->course_id}
    AND i.itemtype = 'crosslink'
    AND i.plugininfo IS NOT NULL AND i.plugininfo != ''
    AND m.name='resourcepage'");
        while(!$rs->EOF) {
            $beforeid=$rs->fields['plugininfo'];
            $rec = backup_getid($restoresettings->backup_unique_code,"course_modules",$beforeid);
            if(!empty($rec->new_id)) {
                // This should always be true as we only support crosslinks to same course
                $this->do_sql("UPDATE modprefix_items SET crosslink={$rec->new_id}, plugininfo=NULL WHERE id={$rs->fields['id']}");
            } else {
                if (!defined('RESTORE_SILENTLY')) {
                    print_object("WARNING: crosslink to non existent course module original id '".$beforeid."', linked to new course module id of current new resourcepage'".$rs->fields['cmid']."'"); //Debug
                }
                $this->do_sql("UPDATE modprefix_items SET crosslink={$rs->fields['cmid']}, plugininfo=NULL WHERE id={$rs->fields['id']}");
            }

            $rs->MoveNext();
        }

        // Now transfer any parentresourcepageid's from an old backup to the course_modules table.
        if (!empty($SESSION->resourcepagerestoreparents)) {
            $rpmoduleid = get_field('modules', 'id', 'name', 'resourcepage');
            foreach ($SESSION->resourcepagerestoreparents as $newchildid => $oldparentid) {
                $newparentid = backup_getid($restoresettings->backup_unique_code,"resourcepage",$oldparentid);
                $this->do_sql("UPDATE prefix_course_modules SET parentcmid =
                        (SELECT id FROM prefix_course_modules WHERE module = $rpmoduleid AND instance = $newparentid)
                        WHERE module = $rpmoduleid AND instance = $newchildid;");
            }
        }

        $tw->commit();
    }

    public function restore_log_entry($restoresettings,$log) {
        //Depending of the action, we recode different things
        switch ($log->action) {
        case 'update':
        case 'add':
        case 'view':
            if ($log->cmid) {
                //Get the new_id of the module (to recode the info field)
                $mod = backup_getid($restoresettings->backup_unique_code,$log->module,$log->info);
                if ($mod) {
                    $log->url = "view.php?id=".$log->cmid;
                    $log->info = $mod->new_id;
                    return $log;
                }
            }
            return false;
        case 'view all':
            $log->url = "index.php?id=".$log->course;
            return $log;
        // Custom ones - same but we don't mess with info (any item codes in info
        // may now be wrong - aw diddums)
        case "addext":
        case "additem":
        case "containeritem":
        case "edititem":
        case "addext":
        case "updateext":
        case "updateph":
            if ($log->cmid) {
                // Get the new_id of the module (to recode the info field)
                $log->url = "view.php?id=".$log->cmid;
                return $log;
            }
            return false;
        default:
            if (!defined('RESTORE_SILENTLY')) {
                echo "action (".$log->module."-".$log->action.") unknown. Not restored<br />"; //Debug
            }
            return false;
        }
    }


    /**
     * gets all continers including original container and its subcointainers
     * returns a result set of container details
     *
     */
    public function get_all_subcontainers($containerid){

        $sql = 'SELECT subcontainerid FROM prefix_resourcepage_containeritems WHERE containerid='.$containerid.'
            AND subcontainerid IS NOT NULL
            UNION
            SELECT ci2.subcontainerid
            FROM prefix_resourcepage_containeritems ci1
            INNER JOIN prefix_resourcepage_containeritems ci2 ON
            ci2.containerid=ci1.subcontainerid
            WHERE ci1.containerid='.$containerid.' AND ci2.subcontainerid IS NOT NULL';

        $rs=$this->do_sql($sql);

        return $rs;


    }

    /**
     * updates all continers details including original container and its subcointainers
     * updates resource page details where resourcepage id is given
     */
    public function update_all_containers_details($containerid, $resourcepageid){

        $sql = 'UPDATE prefix_resourcepage_containers SET resourcepageid='.$resourcepageid.' WHERE
            id IN (
            SELECT subcontainerid FROM prefix_resourcepage_containeritems WHERE containerid='.$containerid.'
            AND subcontainerid IS NOT NULL
            UNION
            SELECT ci2.subcontainerid
            FROM prefix_resourcepage_containeritems ci1
            INNER JOIN prefix_resourcepage_containeritems ci2 ON
            ci2.containerid=ci1.subcontainerid
            WHERE ci1.containerid='.$containerid.' AND ci2.subcontainerid IS NOT NULL)';

            $rs=$this->do_sql($sql);

    }



    public static $inst;
}

module_resourcepage::$inst=new module_resourcepage();

?>